﻿using System.Collections.Generic;
using System.Linq;
using System.Net;
using JetBlack.Collections.Specialized;

namespace JetBlack.AuthFeedBus.Adapters
{
    /// <summary>
    /// A class describing the entitlements of a client to a data item.
    /// </summary>
    public class ClientEntitlement
    {
        /// <summary>
        /// If <c>true</c> the entitlement applies at the field name level.
        /// </summary>
        public readonly bool IsFieldLevel;

        /// <summary>
        /// The field level entitlements.
        /// </summary>
        public readonly IDictionary<string, object> FieldEntitlements;

        /// <summary>
        /// The constructor.
        /// </summary>
        /// <param name="isFieldLevel">If <c>true</c> the entitlement applies at the field name level.</param>
        /// <param name="fieldEntitlements">The field level entitlements.</param>
        public ClientEntitlement(bool isFieldLevel, IDictionary<string, object> fieldEntitlements)
        {
            IsFieldLevel = isFieldLevel;
            FieldEntitlements = fieldEntitlements;
        }
    }

    /// <summary>
    /// This is a convenience class which contains a <see cref="Client"/> instance, the purpose of which is to simplify the construction of broadcast and selet feed servers.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         The caching publisher class wraps up the functionality of a publisher. In it&apos;s simplest form only two methods (<see cref="AddNotification"/> and <see cref="Publish"/>) need be called.
    ///     </para>
    ///     <para>
    ///         There are two tasks all publishers should perform:
    ///         <list type="bullet">
    ///             <item>
    ///                 <description>When a client initially subscribes, send an image of the current set of data on the topic</description>
    ///             </item>
    ///             <item>
    ///                 <description>When new data is received, only the &quot;delta&quot; (i.e. the changed data) should be sent to the subscribers.</description>
    ///             </item>
    ///         </list>
    ///         By caching the feed/topic data this publisher can achive the above tasks.
    ///     </para>
    ///     <para>
    ///         The first step is to indicate which topic patterns are supported by the publisher. This is done by adding notifications to the patterns. The seond step is to publish the data.
    ///         The caching layer handles the distribution of images and deltas to the subscribers.
    ///     </para>
    ///     <para>
    ///         By overriding the <see cref="FetchData"/> and <see cref="CancelData"/> methods a &quot;select-feed&quot; may be implemented. This is a feed which only serves that data to which clients
    ///         have subscribed.
    ///     </para>
    /// </remarks>
    public class CachingPublisher
    {
        /// <summary>
        /// This event is raised when data has been received.
        /// </summary>
        /// <remarks>
        /// Typically this event will not be used, as the publisher will usually be generating the data. However,
        /// a feed concentrator or repeater could use this functionality.
        /// </remarks>
        public event DataCallback OnData;

        /// <summary>
        /// An event raised when the publisher disconnects.
        /// </summary>
        public event ClosedCallback OnClosed;

        private class ClientDataRequest
        {
            public readonly string Feed;
            public readonly string Topic;
            public readonly bool Start;

            public ClientDataRequest(string feed, string topic, bool start)
            {
                Feed = feed;
                Topic = topic;
                Start = start;
            }
        }

        private class ClientEntitlementRequest
        {
            public readonly int ClientId;
            public readonly IPAddress IPAddress;
            public readonly string User;
            public readonly string Feed;
            public readonly string Topic;

            public ClientEntitlementRequest(int clientId, IPAddress ipAddress, string user, string feed, string topic)
            {
                ClientId = clientId;
                IPAddress = ipAddress;
                User = user;
                Feed = feed;
                Topic = topic;
            }
        }

        private AsynchQueue<ClientDataRequest> clientDataRequestQueue = new AsynchQueue<ClientDataRequest>(false);
        private AsynchQueue<ClientEntitlementRequest> clientEntitlementRequestQueue = new AsynchQueue<ClientEntitlementRequest>(false);

        private class ClientState
        {
            public ClientEntitlement Entitlements;
            public bool IsPublished;
        }

        private class CacheItem
        {
            public readonly Dictionary<int, ClientState> ClientStates = new Dictionary<int, ClientState>();
            public readonly Dictionary<string, object> Data = new Dictionary<string, object>();
            public bool IsPublishable = false;
        }

        private class TopicCache : Dictionary<string, CacheItem> { }
        private class FeedCache : Dictionary<string, TopicCache> { }

        private readonly FeedCache feedCache = new FeedCache();
        private readonly Client client = new Client();

        /// <summary>
        /// Construct a caching publisher.
        /// </summary>
        public CachingPublisher()
        {
            client.OnForwardedSubscription += OnForwardedSubscription;
            client.OnData += RaiseOnData;
            client.OnClosed += RaiseOnClosed;
            clientDataRequestQueue.OnItemDequeued += OnDataRequestItemDequeued;
            clientEntitlementRequestQueue.OnItemDequeued += OnEntitlementRequestItemDequeued;
        }

        private void OnDataRequestItemDequeued(ClientDataRequest request)
        {
            if (request.Start)
            {
                // This occurs when an item has been requested for the first time.
                var data = FetchData(request.Feed, request.Topic);
                Publish(request.Feed, request.Topic, true, data);
            }
            else
            {
                // This occurs when the last request has been removed.
                CancelData(request.Feed, request.Topic);
            }
        }

        private void OnEntitlementRequestItemDequeued(ClientEntitlementRequest request)
        {
            var entitlements = FetchEntitlements(request.IPAddress, request.User, request.Feed, request.Topic);
            lock (feedCache)
            {
                TopicCache topicCache;
                if (!feedCache.TryGetValue(request.Feed, out topicCache))
                    return;

                CacheItem cacheItem;
                if (!topicCache.TryGetValue(request.Topic, out cacheItem))
                    return;

                ClientState clientState;
                if (!cacheItem.ClientStates.TryGetValue(request.ClientId, out clientState))
                    return;

                clientState.Entitlements = entitlements;
            }

            client.Entitle(request.ClientId, request.Feed, request.Topic, entitlements.IsFieldLevel, entitlements.FieldEntitlements);

            Publish(request.Feed, request.Topic, true, null);
        }

        /// <summary>
        /// This method can be overriden by a client to provide entitlement information.
        /// </summary>
        /// <param name="iPAddress">The IP address of the client requesting the data.</param>
        /// <param name="user">The user name of the client requesting the data.</param>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        /// <returns>A description of the clients entitlements to the data.</returns>
        protected virtual ClientEntitlement FetchEntitlements(IPAddress iPAddress, string user, string feed, string topic)
        {
            return new ClientEntitlement(false, null);
        }

        /// <summary>
        /// This method can be overridden to by a select feed style publisher to provide new data.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        /// <returns>A dictionary of data.</returns>
        protected virtual Dictionary<string, object> FetchData(string feed, string topic)
        {
            return null;
        }

        /// <summary>
        /// This method can be overridden to by a select feed style publisher to cease the production of data.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        protected virtual void CancelData(string feed, string topic)
        {
        }

        private void RaiseOnClosed(bool isAbnormal)
        {
            clientDataRequestQueue.Close();
            clientEntitlementRequestQueue.Close();

            if (OnClosed != null)
                OnClosed(isAbnormal);
        }

        private void OnForwardedSubscription(int clientId, IPAddress ipAddress, string user, string feed, string topic, bool isAdd)
        {
            if (isAdd)
                AddItem(clientId, ipAddress, user, feed, topic);
            else
                RemoveItem(clientId, ipAddress, user, feed, topic);
        }

        private void AddItem(int clientId, IPAddress ipAddress, string user, string feed, string topic)
        {
            lock (feedCache)
            {
                TopicCache topicCache;
                // Have we received a subscription or published data on this feed yet?
                if (!feedCache.TryGetValue(feed, out topicCache))
                    feedCache.Add(feed, topicCache = new TopicCache());

                CacheItem cacheItem;
                // Have we received a subscription or published data on this feed/topic yet?
                if (!topicCache.TryGetValue(topic, out cacheItem))
                {
                    topicCache.Add(topic, cacheItem = new CacheItem());
                    clientDataRequestQueue.Enqueue(new ClientDataRequest(feed, topic, true));
                }

                // Has this client already subscribed to this feed/topic?
                if (!cacheItem.ClientStates.ContainsKey(clientId))
                {
                    // Add the client to the cache item, and indicate that we have not yet sent an image.
                    cacheItem.ClientStates.Add(clientId, new ClientState { IsPublished = false });
                    clientEntitlementRequestQueue.Enqueue(new ClientEntitlementRequest(clientId, ipAddress, user, feed, topic));
                }

                if (cacheItem.IsPublishable && !cacheItem.ClientStates[clientId].IsPublished)
                {
                    // Send the image and mark this client appropriately.
                    cacheItem.ClientStates[clientId].IsPublished = true;
                    client.Send(clientId, feed, topic, true, cacheItem.Data);
                }
            }
        }

        private void RemoveItem(int clientId, IPAddress ipAddress, string user, string feed, string topic)
        {
            lock (feedCache)
            {
                // Is this feed known?
                TopicCache topicCache;
                if (!feedCache.TryGetValue(feed, out topicCache))
                    return;

                CacheItem cacheItem;
                // Is this topic known?
                if (!topicCache.TryGetValue(topic, out cacheItem))
                    return;

                // Had this client subscribed?
                if (!cacheItem.ClientStates.ContainsKey(clientId))
                    return;

                // Remove the client
                cacheItem.ClientStates.Remove(clientId);

                // If there are no subscribing clients remove the topic.
                if (cacheItem.ClientStates.Count == 0)
                {
                    topicCache.Remove(topic);

                    if (topicCache.Count == 0)
                        feedCache.Remove(feed);

                    clientDataRequestQueue.Enqueue(new ClientDataRequest(feed, topic, false));
                }
            }
        }

        /// <summary>
        /// Connect to a distributor.
        /// </summary>
        /// <param name="host">The host on which the distributor is running.</param>
        /// <param name="port">The port on which the distributor is listening.</param>
        /// <returns>On success <c>true</c> is returned; otherwise <c>false</c>.</returns>
        public bool Connect(string host, int port)
        {
            if (client.Connect(host, port))
            {
                clientDataRequestQueue.StartProcessing();
                clientEntitlementRequestQueue.StartProcessing();
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Close the publisher.
        /// </summary>
        public void Close()
        {
            clientDataRequestQueue.Close();
            clientEntitlementRequestQueue.Close();
            client.Close();
        }

        /// <summary>
        /// Publish data.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        /// <param name="data">The data.</param>
        public void Publish(string feed, string topic, IDictionary<string, object> data)
        {
            Publish(feed, topic, false, data);
        }

        private void Publish(string feed, string topic, bool isImage, IDictionary<string, object> data)
        {
            lock (feedCache)
            {
                // If the feed is not in the cache add it.
                TopicCache topicCache;
                if (!feedCache.TryGetValue(feed, out topicCache))
                    feedCache.Add(feed, topicCache = new TopicCache());

                // If the topic is not in the cache add it.
                CacheItem cacheItem;
                if (!topicCache.TryGetValue(topic, out cacheItem))
                    topicCache.Add(topic, cacheItem = new CacheItem());

                // Bring the cache data up to date.
                if (data != null)
                    foreach (KeyValuePair<string, object> item in data)
                        cacheItem.Data[item.Key] = item.Value;

                // Are there any clients yet to receive any message on this feed/topic?
                if (cacheItem.ClientStates.Values.Any(c => !c.IsPublished)) // TODO: cache this.
                {
                    // Yes. Deliver an idividual message to all 
                    foreach (var clientId in cacheItem.ClientStates.Keys.ToArray()) // TODO: can I remove this now?
                    {
                        if (cacheItem.ClientStates[clientId].IsPublished)
                            client.Send(clientId, feed, topic, isImage, data);
                        else
                        {
                            client.Send(clientId, feed, topic, true, cacheItem.Data);
                            cacheItem.ClientStates[clientId].IsPublished = true;
                        }
                    }
                }
                else
                    client.Publish(feed, topic, isImage, data);
            }
        }

        /// <summary>
        /// Request data.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        public void AddSubscription(string feed, string topic)
        {
            client.AddSubscription(feed, topic);
        }

        /// <summary>
        /// Remove a data subscription.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="topic">The topic.</param>
        public void RemoveSubscription(string feed, string topic)
        {
            client.RemoveSubscription(feed, topic);
        }

        /// <summary>
        /// Ask the distributor to forward client subscription requests.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="isPublisher">If <c>true</c> this client will act as a publisher for this feed.</param>
        public void AddNotification(string feed, bool isPublisher)
        {
            client.AddNotification(feed, isPublisher);
        }

        /// <summary>
        /// Remove a notification request.
        /// </summary>
        /// <param name="feed">The feed.</param>
        /// <param name="isPublisher">If <c>true</c> this client will act as a publisher for this feed.</param>
        public void RemoveNotification(string feed, bool isPublisher)
        {
            client.RemoveNotification(feed, isPublisher);
        }

        private void RaiseOnData(string feed, string topic, IDictionary<string, object> data, bool isImage)
        {
            if (OnData != null)
                OnData(feed, topic, data, isImage);
        }
    }
}
